/*
 * Created on 21 Jun 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package controller.networkmanager.tcp;


import java.net.InetAddress;
import java.nio.channels.CancelledKeyException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import controller.config.COConfigurationManager;
import controller.config.ParameterListener;
import torrentlib.AEThread2;
import torrentlib.Debug;
import torrentlib.SystemTime;

import controller.networkmanager.VirtualChannelSelector;
import torrentlib.stats.AzureusCoreStats;
import torrentlib.stats.AzureusCoreStatsProvider;

public class 
TCPNetworkManager 
{  
	private static int WRITE_SELECT_LOOP_TIME 		= 25;
	private static int WRITE_SELECT_MIN_LOOP_TIME 	= 0;
	private static int READ_SELECT_LOOP_TIME		= 25;
	private static int READ_SELECT_MIN_LOOP_TIME	= 0;
	
	protected static int tcp_mss_size;
	  
	private static final TCPNetworkManager instance = new TCPNetworkManager();

	public static TCPNetworkManager getSingleton(){ return( instance ); }

	public static boolean TCP_INCOMING_ENABLED;
	public static boolean TCP_OUTGOING_ENABLED;
	
	static{
		COConfigurationManager.addAndFireParameterListeners(
				new String[]{
					"TCP.Listen.Port.Enable",
					"network.tcp.connect.outbound.enable"
				},
				new ParameterListener()
				{
					public void 
					parameterChanged(
						String name )
					{
						TCP_INCOMING_ENABLED = TCP_OUTGOING_ENABLED = COConfigurationManager.getBooleanParameter( "TCP.Listen.Port.Enable" );
						
						if ( TCP_OUTGOING_ENABLED ){
							
							TCP_OUTGOING_ENABLED = COConfigurationManager.getBooleanParameter( "network.tcp.connect.outbound.enable" );
						}
					}
				});
		
		COConfigurationManager.addAndFireParameterListeners(
				new String[]{ 
					"network.tcp.read.select.time", 
					"network.tcp.read.select.min.time", 
					"network.tcp.write.select.time",
					"network.tcp.write.select.min.time",
					},
				new ParameterListener()
				{
					public void 
					parameterChanged(
						String name )
					{
						WRITE_SELECT_LOOP_TIME 		= COConfigurationManager.getIntParameter(  "network.tcp.write.select.time" );
						WRITE_SELECT_MIN_LOOP_TIME 	= COConfigurationManager.getIntParameter(  "network.tcp.write.select.min.time" );
						
						READ_SELECT_LOOP_TIME 		= COConfigurationManager.getIntParameter(  "network.tcp.read.select.time" );
						READ_SELECT_MIN_LOOP_TIME 	= COConfigurationManager.getIntParameter(  "network.tcp.read.select.min.time" );
					}
				});
	}
	
	 /**
	   * Get the configured TCP MSS (Maximum Segment Size) unit, i.e. the max (preferred) packet payload size.
	   * NOTE: MSS is MTU-40bytes for TCPIP headers, usually 1460 (1500-40) for standard ethernet
	   * connections, or 1452 (1492-40) for PPPOE connections.
	   * @return mss size in bytes
	   */
	
	public static int getTcpMssSize() {  return tcp_mss_size;  }

	public static void
	refreshRates(
		int		min_rate )
	{
		 tcp_mss_size = COConfigurationManager.getIntParameter( "network.tcp.mtu.size" ) - 40; 	        

	    if( tcp_mss_size > min_rate )  tcp_mss_size = min_rate - 1;
	    
	    if( tcp_mss_size < 512 )  tcp_mss_size = 512; 
	}
	
	private final VirtualChannelSelector read_selector 	= 
			new VirtualChannelSelector( "TCP network manager", VirtualChannelSelector.OP_READ, true );
	private final VirtualChannelSelector write_selector = 
			new VirtualChannelSelector( "TCP network manager", VirtualChannelSelector.OP_WRITE, true );

	private final TCPConnectionManager connect_disconnect_manager = new TCPConnectionManager();

	private final IncomingSocketChannelManager incoming_socketchannel_manager = 
		new IncomingSocketChannelManager( "TCP.Listen.Port", "TCP.Listen.Port.Enable" );	  

	private long	read_select_count;
	private long	write_select_count;
	
	
	protected
	TCPNetworkManager()
	{
		Set	types = new HashSet();
		
		types.add( AzureusCoreStats.ST_NET_TCP_SELECT_READ_COUNT );
		types.add( AzureusCoreStats.ST_NET_TCP_SELECT_WRITE_COUNT );

		AzureusCoreStats.registerProvider(
			types,
			new AzureusCoreStatsProvider()
			{
				public void
				updateStats(
					Set		types,
					Map		values )
				{
					if ( types.contains( AzureusCoreStats.ST_NET_TCP_SELECT_READ_COUNT )){
						
						values.put( AzureusCoreStats.ST_NET_TCP_SELECT_READ_COUNT, new Long( read_select_count ));
					}	
					if ( types.contains( AzureusCoreStats.ST_NET_TCP_SELECT_WRITE_COUNT )){
						
						values.put( AzureusCoreStats.ST_NET_TCP_SELECT_WRITE_COUNT, new Long( write_select_count ));
					}					
				}
			});
		
		   //start read selector processing
		
		AEThread2 read_selector_thread = 
	    	new AEThread2( "ReadController:ReadSelector", true ) 
	    	{
		    	public void 
		    	run() 
		    	{
		    		while( true ) {
		    	
		    			try{
		    				if ( READ_SELECT_MIN_LOOP_TIME > 0 ){
		    					
		    					long	start = SystemTime.getHighPrecisionCounter();
		    					
		    					read_selector.select( READ_SELECT_LOOP_TIME );
		    					
		    					long duration = SystemTime.getHighPrecisionCounter() - start;
		    					
		    					duration = duration/1000000;
		    					
		    					long	sleep = READ_SELECT_MIN_LOOP_TIME - duration;
		    					
		    					if ( sleep > 0 ){
		    								    						
		    						try{   							
		    							Thread.sleep( sleep );
		    							
		    						}catch( Throwable e ){		
		    						}
		    					}
		    				}else{
		    					
			    				read_selector.select( READ_SELECT_LOOP_TIME );
		    				}
		    				
			    			read_select_count++;
			    				
		    			}catch( Throwable t ) {
		    				
		    					// filter out the boring ones
		    				
		    				if (!( t instanceof CancelledKeyException )){
		    				
		    					Debug.out( "readSelectorLoop() EXCEPTION: ", t );
		    				}
		    			}      
		    		}
		    	}
	    	};
	    	
	    read_selector_thread.setPriority( Thread.MAX_PRIORITY - 2 );
	    read_selector_thread.start();
	    
	    	//start write selector processing
	    
	    AEThread2 write_selector_thread = 
	    	new AEThread2( "WriteController:WriteSelector", true )
	    	{
		    	public void 
		    	run() 
		    	{
		    	    while( true ){
		    	    	
		    	    	try{
		    	    		if ( WRITE_SELECT_MIN_LOOP_TIME > 0 ){
		    					
		    					long	start = SystemTime.getHighPrecisionCounter();
		    					
		    					write_selector.select( WRITE_SELECT_LOOP_TIME );
		    					
		    					long duration = SystemTime.getHighPrecisionCounter() - start;
		    					
		    					duration = duration/1000000;
		    					
		    					long	sleep = WRITE_SELECT_MIN_LOOP_TIME - duration;
		    					
		    					if ( sleep > 0 ){
		    								    						
		    						try{   							
		    							Thread.sleep( sleep );
		    							
		    						}catch( Throwable e ){		
		    						}
		    					}
		    	    		}else{
		    	    			
		    	    			write_selector.select( WRITE_SELECT_LOOP_TIME );
		    	    		
		    	    			write_select_count++;
		    	    		}
		    	    	}catch( Throwable t ) {
		    	    		
		    	    		Debug.out( "writeSelectorLoop() EXCEPTION: ", t );
		    	    	}      
		  		    }
		    	}
	    	};
	    	
	    write_selector_thread.setPriority( Thread.MAX_PRIORITY - 2 );
	    write_selector_thread.start();	    
	}
	
	public void
	setExplicitBindAddress(
			InetAddress	address )
	{
		incoming_socketchannel_manager.setExplicitBindAddress( address );
	}

	public void
	clearExplicitBindAddress()
	{
		incoming_socketchannel_manager.clearExplicitBindAddress();
	}

	public boolean
	isEffectiveBindAddress(
			InetAddress		address )
	{
		return( incoming_socketchannel_manager.isEffectiveBindAddress( address ));
	}
	
		/**
		 * Get the socket channel connect / disconnect manager.
		 * @return connect manager
		 */
	
	public TCPConnectionManager 
	getConnectDisconnectManager() 
	{  
		return connect_disconnect_manager;  
	}

	/**
	 * Get the virtual selector used for socket channel read readiness.
	 * @return read readiness selector
	 */
	public VirtualChannelSelector getReadSelector() {  return read_selector;  }


	/**
	 * Get the virtual selector used for socket channel write readiness.
	 * @return write readiness selector
	 */
	public VirtualChannelSelector getWriteSelector() {  return write_selector;  }


	public boolean
	isTCPListenerEnabled()
	{
		return( incoming_socketchannel_manager.isEnabled());
	}

	/**
	 * Get port that the TCP server socket is listening for incoming connections on.
	 * @return port number
	 */
	
	public int 
	getTCPListeningPortNumber() 
	{  
		return( incoming_socketchannel_manager.getTCPListeningPortNumber());  
	}
	
	public long
	getLastIncomingNonLocalConnectionTime()
	{
		return( incoming_socketchannel_manager.getLastNonLocalConnectionTime());
	}
}
